Repository navigation
Conversation
http2.connect() throws ERR_HTTP2_UNSUPPORTED_PROTOCOL with Node's message, and only when it opens the socket itself. With options.createConnection the protocol is not checked. request() takes its default :scheme from the protocol the session was connected with, minus the trailing colon. A protocol in the request() options no longer changes it. Co-authored-by: Meghan Denny <hello@nektro.net>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review. WalkthroughThe change adds HTTP/2 unsupported-protocol error handling, restricts client connections to ChangesHTTP/2 protocol handling
Suggested reviewers: Priority: ⬇️ Low 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 12:18 AM PT - Sep 20th, 2026
✅ @robobun, your commit ae43c6a19bd0b5f36f2058cf00f8b32d2e1c5fc0 passed in 🧪 To try this PR locally: bunx bun-pr 43505That installs a local version of the PR into your bun-43505 --bun |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes observable node:http2 behavior (the throw now happens only when http2 opens the socket itself, and a request-level protocol option no longer affects :scheme) and touches src/js/builtins.d.ts, which has a designated code owner, a human look is still worthwhile.
What was reviewed:
- Compared the new
connectWithProtocolswitch and constructor ordering against Node v26.3.0core.jsconnect(): the check sits in the non-createConnectionbranch there too, and theprotocol "<p>" is unsupported.text matches Node'sERR_HTTP2_UNSUPPORTED_PROTOCOLmessage; thesimpleErrorMessagesrow takes 1 argument, matching the single call site and the new.d.tsdeclaration. - Confirmed both request paths (object headers and raw
[name, value, ...]array) now read the same#defaultScheme, and that#urlhad no remaining readers after the removal. - Checked that nothing between the old throw site and the new one (authority string,
onConnectclosure) acquires a socket or timer, so the later throw does not leak a resource;#defaultSchemewithout an initializer matches the neighboring#authority/#socket_proxyfields. - The only other suite assertion on this code (
test/js/node/test/parallel/test-http2-connect.js:127) checks code and name only, so it is unaffected by the message change.
Extended reasoning...
Overview
The PR touches four files. src/js/node/http2.ts moves the unsupported-protocol throw from the top of the ClientHttp2Session constructor into connectWithProtocol (reached only when options.createConnection is not a function), replaces the stored #url with a precomputed #defaultScheme (the connect-time protocol minus its trailing colon, via a newly hoisted StringPrototypeSlice primordial), and makes both request() header paths use that field instead of re-deriving the scheme from this.#url.protocol || options?.protocol where options was accidentally the per-request options. src/jsc/bindings/ErrorCode.cpp adds a simpleErrorMessages row so $ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol) produces Node's message instead of echoing the argument; src/js/builtins.d.ts adds the matching typed declaration. The test file adds a describe block with four tests covering the throw (string/URL/object authority, options.protocol, non-function createConnection), the createConnection bypass with :scheme: ftp observed server-side over a duplexPair, options.protocol: "http:" becoming the default scheme, and a request-level protocol option not overriding the session scheme.
Security risks
None identified. The relaxed check only applies when the caller supplies their own transport via createConnection, in which case the protocol string is only used as the :scheme pseudo-header value; this mirrors Node exactly. No user input reaches native code differently than before, and the C++ change is a constexpr table entry with a fixed argument count of 1 that matches the single call site.
Level of scrutiny
Moderate. The diff is small and closely tracks Node's connect() in lib/internal/http2/core.js, which I compared against directly: Node computes protocol = authority.protocol || options.protocol || 'https:', then only reaches its switch (protocol) (with default: throw new ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol)) in the else branch of typeof options.createConnection === 'function', and request() sets :scheme from session[kProtocol].slice(0, -1). The refactor removed two divergent copy-pasted scheme derivations in favor of one field, which is the right direction. I verified that the code path between the old throw location and the new one allocates no socket or timer, so a throw from connectWithProtocol does not leak anything the earlier throw would have avoided. The mock/duplexPair helpers used by the tests are already imported in the file. Still, this is an observable behavior change in a node:* module (two inputs that previously threw or sent a different scheme now behave differently), and src/js/builtins.d.ts matches a *.d.ts CODEOWNERS entry, so a human sign-off is appropriate rather than an automated approve.
Other factors
The four candidate issues raised during the hunt were all Node-parity consequences rather than defects: the request-level protocol override and the createConnection bypass are exactly Node's semantics, the uninitialized #defaultScheme field matches sibling private fields in the same class, and non-string protocol values are coerced the same way Node's slice call coerces them. The only pre-existing suite assertion on this error (test-http2-connect.js) checks code and name, not message, so no existing test was weakened. One pre-existing divergence not introduced here: Bun defaults the port using the merged protocol, while Node uses only authority.protocol; the PR does not claim to address that.
|
Status Reproduced on bun 1.4.3-canary.1+367d939d9 against Node.js v26.3.0 with the script in the description ( The four tests in PR: #43505 |
The tests do not depend on anything in node-http2.test.js. On their own they run in about 5 seconds on a debug build.
Problem
http2.connect("ftp://127.0.0.1:1")throwsERR_HTTP2_UNSUPPORTED_PROTOCOLwith the messageftp:. Node.js v26.3.0 saysprotocol "ftp:" is unsupported.ErrorCode.cpphas no message text for this code, so the first argument becomes the message.ClientHttp2Sessionconstructor (src/js/node/http2.ts) checks the protocol before it looks atoptions.createConnection. Node.js checks it only when it opens the socket itself (connect()). Sohttp2.connect("ftp://host", { createConnection })throws in Bun only.request()takes the default:schemefrom the URL, then from its own options.http2.connect({ port }, { protocol: "http:" })sends:scheme: https. Node.js sendshttp.Fix
simpleErrorMessagestable. Move the protocol switch intoconnectWithProtocol, which runs only whencreateConnectionis not a function. This supersedes node:http2: add throw for ERR_HTTP2_UNSUPPORTED_PROTOCOL #17927, which has the same placement.:scheme: the connect-time protocol without the trailing:(as in Node.js).request()reads it for both header forms (an object, a raw[name, value, ...]array). Without this, a custom protocol would send:scheme: ftp:.protocolin therequest()options no longer changes:scheme. Node.js has no such option. The#urlfield had no other reader, so it is gone.test/js/node/http2/node-http2-connect-protocol.test.ts(4 tests, a debug build of main fails all 4, Node.js passes the same scenarios). Also 14 upstreamtest-http2-*files. Self-reviewed: 5 concerns raised, 5 addressed (see Notes).Background
http2.connect(authority, options)opens a client session. The protocol isauthority.protocol, thenoptions.protocol, thenhttps:.options.createConnectionlets the caller supply the transport. The protocol then only names the:scheme.$ERR_*calls in built-in JS go throughjsFunctionMakeErrorWithCode. A code with nocasethere and no table row uses its first argument as the message.Notes
Repro (run with
bun file.cjsandnode file.cjs):protocol "ftp:" is unsupported.ftp:protocol "ftp:" is unsupported.ftp://withcreateConnectionftpERR_HTTP2_UNSUPPORTED_PROTOCOLftp{ protocol: "http:" }in theconnect()optionshttphttpshttp{ protocol: "http:" }in therequest()options, defaulthttps:sessionhttpshttphttpsnode-http2.test.js, and alone they run in about 5 seconds on a debug build.node-http2.test.jstakes over 6 minutes there, and several of its subprocess tests pass the 5 second limit on a debug build of main too.bun bd test test/js/node/http2/node-http2-connect-protocol.test.tswithsrc/at main gives 0 pass, 4 fail. Withsrc/at this branch it gives 4 pass.src/bun.js/paths that no longer exist) puts the throw inconnectWithProtocoland adds the same message. Its review says "LGTM but needs a test". The throw at the top of the constructor came later, from compat(http2) validations and some behavior fixes #19558. This PR has the test.builtins.d.tsgets the typed declaration, like the other codes whose message takes arguments.request()code readoptions?.protocolby accident: it meant theconnect()options, butoptionsthere is the second argument ofrequest().port, Node.js builds the authoritylocalhost:undefined. Bun keepslocalhost:443.ERR_HTTP_TRAILER_INVALIDandERR_HTTP_CONTENT_LENGTH_MISMATCH(node:http),ERR_INVALID_URL_SCHEME(url.fileURLToPathwith thewindowsoption) andERR_SCRIPT_EXECUTION_INTERRUPTED(REPL). They are not in this PR on purpose: their call sites are in other modules, and one needs a change inNodeHTTPResponse.rs. node: give seven ERR_* codes Node's message text #43502 fixed them and is merged. This branch has main merged in, and the two changes do not conflict. node: fix broken error messages for ERR_HTTP2_UNSUPPORTED_PROTOCOL and 4 others #35791 and node:http: fix ERR_HTTP_CONTENT_LENGTH_MISMATCH and ERR_HTTP_TRAILER_INVALID message text #35777 (both closed unmerged) were earlier attempts.request()options.requestJSONin the test no longer parses inside an event callback. This description now names what node:http2: add throw for ERR_HTTP2_UNSUPPORTED_PROTOCOL #17927 and node:http: fix ERR_HTTP_CONTENT_LENGTH_MISMATCH and ERR_HTTP_TRAILER_INVALID message text #35777 did, and therequest()options change moved up into Fix. The sibling codes now point at node: give seven ERR_* codes Node's message text #43502.test-http2-connect.js,test-http2-create-client-connect.js,test-http2-create-client-session.js,test-http2-raw-headers-defaults.js,test-http2-raw-headers.js,test-http2-sent-headers.js,test-http2-generic-streams.js,test-http2-connect-method.js,test-http2-connect-method-extended.js,test-http2-misused-pseudoheaders.js,test-http2-multiheaders-raw.js,test-http2-client-proxy-over-http2.js,test-http2-client-jsstream-destroy.js,test-http2-https-fallback.js. Alsonode-http2-client-close.test.ts,node-http2-continuation.test.ts,node-http2-settings-ack-ordering.test.tsandtest/js/node/errors/error-code-messages.test.ts.[human-review] gate passed · iteration 1 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 1 rejected · iteration 1
evidence per changed file